package sk.tuke.constraintbuilder.analyzer;

import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.StringTokenizer;
import java.util.regex.Pattern;

import javax.xml.transform.OutputKeys;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.stream.StreamResult;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.jdt.core.IAnnotation;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IField;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.ILocalVariable;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.ISourceReference;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.AnnotationTypeMemberDeclaration;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.EnumConstantDeclaration;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.IAnnotationBinding;
import org.eclipse.jdt.core.dom.IExtendedModifier;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MarkerAnnotation;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints2.ReturnTypeVariable2;
import org.xml.sax.InputSource;

import sk.tuke.constraintbuilder.builder.resourcemodel.AnnotationElement;
import sk.tuke.constraintbuilder.builder.resourcemodel.AnnotationMemberElement;
import sk.tuke.constraintbuilder.builder.resourcemodel.AnnotationMembersElement;
import sk.tuke.constraintbuilder.builder.resourcemodel.ConstructorElement;
import sk.tuke.constraintbuilder.builder.resourcemodel.ContainerElement;
import sk.tuke.constraintbuilder.builder.resourcemodel.Element;
import sk.tuke.constraintbuilder.builder.resourcemodel.EnumConstElement;
import sk.tuke.constraintbuilder.builder.resourcemodel.EnumElement;
import sk.tuke.constraintbuilder.builder.resourcemodel.ExtendsElement;
import sk.tuke.constraintbuilder.builder.resourcemodel.FieldElement;
import sk.tuke.constraintbuilder.builder.resourcemodel.FieldsElement;
import sk.tuke.constraintbuilder.builder.resourcemodel.ImplementsElement;
import sk.tuke.constraintbuilder.builder.resourcemodel.InterfaceElement;
import sk.tuke.constraintbuilder.builder.resourcemodel.MethodElement;
import sk.tuke.constraintbuilder.builder.resourcemodel.MethodsElement;
import sk.tuke.constraintbuilder.builder.resourcemodel.OutputElement;
import sk.tuke.constraintbuilder.builder.resourcemodel.ParamElement;
import sk.tuke.constraintbuilder.builder.resourcemodel.ParamsElement;
import sk.tuke.constraintbuilder.builder.resourcemodel.TypeAnnotationsElement;
import sk.tuke.constraintbuilder.builder.resourcemodel.TypeDeclarationElement;
import sk.tuke.constraintbuilder.utils.MyJdtUtils;
import sk.tuke.constraintbuilder.utils.MyUtils;
import sk.tuke.constraintbuilder.utils.MyPreferences;

public class JavaModelBuilder {

    /**
     * Create a java model representation of given type and save it to file.
     * @param type
     * @return a file that contains java model representation of given type
     * @throws JavaModelException
     */
    public File createResourceModel(IType type) throws JavaModelException {   	
        
    	OutputElement output = new OutputElement();
        String fileName = type.getResource().getName();
        String filePath = type.getResource().getFullPath().toOSString();
        output.setFile(filePath);

        Element examinedElement = examine(type);

        if (examinedElement != null) {
        	output.addElement(examinedElement);
        }

        IType[] nestedTypes = type.getTypes();
        for (IType nestedType : nestedTypes) {
        	Element examinedNestedElement = examine(nestedType);
        	if (examinedNestedElement != null) {
        		output.addElement(examinedNestedElement);
            }
        }
        
        String unformattedXML = output.generateXML();
        FileWriter fStream;
        try {
            new File(MyPreferences.JAVA_MODEL_DIR).mkdir();
            String fileNoExt = new StringTokenizer(fileName, ".").nextToken();
            File javaModel = new File(MyPreferences.JAVA_MODEL_DIR + "\\" + fileNoExt + ".xml");
            fStream = new FileWriter(javaModel);
            BufferedWriter out = new BufferedWriter(fStream);
            out.write(formatXML(unformattedXML));
            out.close();
            return javaModel;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private TypeDeclarationElement examine(final IType typeDeclaration) throws JavaModelException {
        
    	TypeDeclarationEnum typeOfDeclaration = null;
        if (typeDeclaration.isEnum()) {
        	typeOfDeclaration = TypeDeclarationEnum.ENUM;
        } else if (typeDeclaration.isInterface()) {
        	typeOfDeclaration = TypeDeclarationEnum.INTERFACE;
        } else if (typeDeclaration.isClass()) {
        	typeOfDeclaration = TypeDeclarationEnum.CLASS;
        } else if (typeDeclaration.isAnnotation()) {
        	typeOfDeclaration = TypeDeclarationEnum.ANNOTATION;
        }
    	
    	final TypeDeclarationElement classElement = new TypeDeclarationElement(typeOfDeclaration.toString(), typeDeclaration.getElementName());
        final FieldsElement fieldsElement = new FieldsElement();
        final TypeAnnotationsElement typeAnnotationsElement = new TypeAnnotationsElement();
        final MethodsElement methodsElement = new MethodsElement();
        
        final List<IType> superTypes = new ArrayList<>();
        final List<TypeDeclarationElement> superTypeDeclarationElements = new ArrayList<>();
        
        ICompilationUnit icu = JavaCore.createCompilationUnitFrom((IFile) typeDeclaration.getResource());
    	ASTParser parser = ASTParser.newParser(AST.JLS3);
    	parser.setResolveBindings(true);
    	parser.setSource(icu);
    	CompilationUnit compilationUnit = (CompilationUnit) parser.createAST(null);

        //<extends>, <implements>, <type_annotations>/////////////////////////////////////////////////////////////
        compilationUnit.accept(new ASTVisitor() {
        	@Override
        	public boolean visit(TypeDeclaration node) {
        		//Superclass
        		Type superclassType = node.getSuperclassType();
                if (superclassType != null) {
                	ITypeBinding binding = superclassType.resolveBinding();
                	if(binding != null){
                        classElement.addElement(new ExtendsElement(binding.getTypeDeclaration().getQualifiedName()));
                        IType superType = MyJdtUtils.findTypeInProject(binding.getTypeDeclaration().getName(), typeDeclaration.getJavaProject());
						if(superType!=null){
							superTypes.add(superType);
							RulesetManager.getInstance().setSubClassOrImplementation(typeDeclaration.getResource(), superType.getResource());
						}
                	}
                }
                //Interfaces
				@SuppressWarnings("unchecked")
				List<Type> superInterfaceTypes = node.superInterfaceTypes();
				if(superInterfaceTypes!=null){
					ImplementsElement implementsElement = new ImplementsElement();
					for (Type superInterface : superInterfaceTypes) {
						ITypeBinding binding = superInterface.resolveBinding();
						if(binding!=null){
							implementsElement.addElement(new InterfaceElement(binding.getTypeDeclaration().getQualifiedName()));
		                    IType superType = MyJdtUtils.findTypeInProject(binding.getTypeDeclaration().getName(), typeDeclaration.getJavaProject());
							if(superType!=null){
								superTypes.add(superType);
								RulesetManager.getInstance().setSubClassOrImplementation(typeDeclaration.getResource(), superType.getResource());
							}
						}
					}
		        	if(!implementsElement.getElements().isEmpty()){
		        		classElement.addElement(implementsElement);
		        	}
				}
        		//Annotations
				for (Object modifier : node.modifiers()) {
					if(modifier instanceof Annotation){
						IAnnotationBinding annotationBinding = ((Annotation)modifier).resolveAnnotationBinding();
						String qualifiedAnnotationName = annotationBinding.getAnnotationType().getQualifiedName();
						int startPosition = ((Annotation) modifier).getStartPosition();
						int endPosition = startPosition + ((Annotation) modifier).getLength();
						int line = getLineNumber(typeDeclaration, startPosition);
						typeAnnotationsElement.addElement(new AnnotationElement(qualifiedAnnotationName, line, startPosition, endPosition));
					}
				}
				return true;
    		 }
    	});
    	if(!typeAnnotationsElement.getElements().isEmpty()){
    		classElement.addElement(typeAnnotationsElement);
    	}
        
        //Examine superclasses or interfaces
        for (IType superType : superTypes) {
			superTypeDeclarationElements.add(examine(superType));
		}

        //<fields>/////////////////////////////////////////////////////////////////////////////////////////////////////////
        compilationUnit.accept(new ASTVisitor() {
        	@Override
        	public boolean visit(FieldDeclaration node) {
        		List<Object> fragments = node.fragments();
        		for (Object object : fragments) {
					if(object instanceof VariableDeclarationFragment){
						VariableDeclarationFragment fragment = (VariableDeclarationFragment) object;
						String fieldName = fragment.getName().getIdentifier();
						String fieldType = node.getType().resolveBinding().getTypeDeclaration().getQualifiedName();
						FieldElement fieldElement = new FieldElement(fieldName, fieldType);
		        		//Get field annotations
						for (Object modifier : node.modifiers()) {
							if(modifier instanceof Annotation){
								IAnnotationBinding annotationBinding = ((Annotation)modifier).resolveAnnotationBinding();
								String qualifiedAnnotationName = annotationBinding.getAnnotationType().getQualifiedName();
								int startPosition = ((Annotation) modifier).getStartPosition();
								int endPosition = startPosition + ((Annotation) modifier).getLength();
								int line = getLineNumber(typeDeclaration, startPosition);
								fieldElement.addElement(new AnnotationElement(qualifiedAnnotationName, line, startPosition, endPosition));
							}
						}
		        		fieldsElement.addElement(fieldElement);
					}
				}
				return true;
    		 }
    	});
		for (TypeDeclarationElement typeDeclarationElement : superTypeDeclarationElements) {
			for (Element element : typeDeclarationElement.getElements()) {
				if(element instanceof FieldsElement){
					fieldsElement.addElements(((FieldsElement) element).getElements());
				}
				
			}
		}
    	if(!fieldsElement.getElements().isEmpty()){
    		classElement.addElement(fieldsElement);
    	}

        //<methods>/////////////////////////////////////////////////////////////////////////////////////////////////////
        compilationUnit.accept(new ASTVisitor() {
        	@Override
        	public boolean visit(MethodDeclaration node) {
        		String methodName = node.getName().getIdentifier();
        		MethodElement methodElement;
        		if(node.isConstructor()){
        			methodElement = new ConstructorElement(methodName);
        		}else{
        			String qTypeName = node.getReturnType2().resolveBinding().getTypeDeclaration().getQualifiedName();
        			methodElement = new MethodElement(methodName, qTypeName);
        		}
        		//Get method annotations
				for (Object modifier : node.modifiers()) {
					if(modifier instanceof Annotation){
						IAnnotationBinding annotationBinding = ((Annotation)modifier).resolveAnnotationBinding();
						String qualifiedAnnotationName = annotationBinding.getAnnotationType().getQualifiedName();
						int startPosition = ((Annotation) modifier).getStartPosition();
						int endPosition = startPosition + ((Annotation) modifier).getLength();
						int line = getLineNumber(typeDeclaration, startPosition);
						methodElement.addElement(
								new AnnotationElement(qualifiedAnnotationName, line, startPosition, endPosition));
					}
				}
        		//Get parameters
    			List<Object> parameters = node.parameters();
    			ParamsElement paramsElement = new ParamsElement();
    			for (Object parameter : parameters) {
    				if(parameter instanceof SingleVariableDeclaration){
    					SingleVariableDeclaration par = (SingleVariableDeclaration) parameter;
    					String parameterName = par.getName().getIdentifier();
    					String parameterType = par.getType().resolveBinding().getTypeDeclaration().getQualifiedName();
    					ParamElement paramElement = new ParamElement(parameterName, parameterType);
    					//Get parameter annotations
    					for (Object modifier : par.modifiers()) {
							if(modifier instanceof Annotation){
								IAnnotationBinding annotationBinding = ((Annotation)modifier).resolveAnnotationBinding();
								String qualifiedAnnotationName = annotationBinding.getAnnotationType().getQualifiedName();
								int startPosition = ((Annotation) modifier).getStartPosition();
								int endPosition = startPosition + ((Annotation) modifier).getLength();
								int line = getLineNumber(typeDeclaration, startPosition);
	    						paramElement.addElement(
	    								new AnnotationElement(qualifiedAnnotationName, line, startPosition, endPosition));
							}
						}
    					paramsElement.addElement(paramElement);
    				}
				}
    			methodElement.addElement(paramsElement);
    			methodsElement.addElement(methodElement);
    			return true;
    		 }
    	});
		for (TypeDeclarationElement typeDeclarationElement : superTypeDeclarationElements) {
			for (Element element : typeDeclarationElement.getElements()) {
				if(element instanceof MethodsElement){
					methodsElement.addElements(((MethodsElement) element).getElements());
				}
				
			}
		}
    	if(!methodsElement.getElements().isEmpty()){
    		classElement.addElement(methodsElement);
    	}
        
        //<enum>/////////////////////////////////////////////////////////////////////////////////////////////////////////
        if(typeOfDeclaration == TypeDeclarationEnum.ENUM){
        	final EnumElement enumElement = new EnumElement();
        	compilationUnit.accept(new ASTVisitor() {
            	@Override
            	public boolean visit(EnumConstantDeclaration node) {
            		String enumConstName = node.getName().getIdentifier();
            		EnumConstElement enumConstElement = new EnumConstElement(enumConstName);
            		//Annotations
					for (Object modifier : node.modifiers()) {
						if(modifier instanceof Annotation){
							IAnnotationBinding annotationBinding = ((Annotation)modifier).resolveAnnotationBinding();
							String qualifiedAnnotationName = annotationBinding.getAnnotationType().getQualifiedName();
							int startPosition = ((Annotation) modifier).getStartPosition();
							int endPosition = startPosition + ((Annotation) modifier).getLength();
							int line = getLineNumber(typeDeclaration, startPosition);
    						enumConstElement.addElement(new AnnotationElement(qualifiedAnnotationName, line, startPosition, endPosition));
						}
					}
            		enumElement.addElement(enumConstElement);
    				return true;
        		 }
        	});
        	if(!enumElement.getElements().isEmpty()){
        		classElement.addElement(enumElement);
        	}
        }
        
        //<annotation_members>/////////////////////////////////////////////////////////////////////////////////////////////////////////
        if(typeOfDeclaration == TypeDeclarationEnum.INTERFACE){
        	final AnnotationMembersElement annotationMembersElement = new AnnotationMembersElement();
        	compilationUnit.accept(new ASTVisitor() {
            	@Override
            	public boolean visit(AnnotationTypeMemberDeclaration node) {
            		String annMemberName = node.getName().getIdentifier();
            		String annMemberType = node.getType().resolveBinding().getTypeDeclaration().getQualifiedName();
            		AnnotationMemberElement annotationMemberElement = new AnnotationMemberElement(annMemberName, annMemberType);
            		//Annotations
					for (Object modifier : node.modifiers()) {
						if(modifier instanceof Annotation){
							IAnnotationBinding annotationBinding = ((Annotation)modifier).resolveAnnotationBinding();
							String qualifiedAnnotationName = annotationBinding.getAnnotationType().getQualifiedName();
							int startPosition = ((Annotation) modifier).getStartPosition();
							int endPosition = startPosition + ((Annotation) modifier).getLength();
							int line = getLineNumber(typeDeclaration, startPosition);
    						annotationMemberElement.addElement(new AnnotationElement(qualifiedAnnotationName, line, startPosition, endPosition));
						}
					}
            		annotationMembersElement.addElement(annotationMemberElement);
    				return true;
        		 }
        	});
        	if(!annotationMembersElement.getElements().isEmpty()){
        		classElement.addElement(annotationMembersElement);
        	}
        }
    
        return classElement;
    }

    public String formatXML(String unformattedXML) {
        try {
            Transformer serializer = SAXTransformerFactory.newInstance().newTransformer();
            serializer.setOutputProperty(OutputKeys.INDENT, "yes");
            serializer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
            Source xmlSource = new SAXSource(new InputSource(new ByteArrayInputStream(unformattedXML.getBytes())));
            StreamResult res = new StreamResult(new ByteArrayOutputStream());
            serializer.transform(xmlSource, res);
            return new String(((ByteArrayOutputStream) res.getOutputStream()).toByteArray());
        } catch (Exception e) {
            return unformattedXML;
        }
    }
    
	private int getAnnotationLineNumber(final IType type, IAnnotation annotation){
	    String source;
		try {
			source = type.getCompilationUnit().getSource();
		    String sourceUpToMethod= source.substring(0, annotation.getSourceRange().getOffset());
		    Pattern lineEnd= Pattern.compile("$", Pattern.MULTILINE | Pattern.DOTALL);
		    return lineEnd.split(sourceUpToMethod).length;
		} catch (JavaModelException e) {
			return -1;
		}
	}
	
	private int getLineNumber(final IType type, int startPosition){
	    String source;
		try {
			source = type.getCompilationUnit().getSource();
		    String sourceUpToMethod= source.substring(0, startPosition);
		    Pattern lineEnd= Pattern.compile("$", Pattern.MULTILINE | Pattern.DOTALL);
		    return lineEnd.split(sourceUpToMethod).length;
		} catch (JavaModelException e) {
			return -1;
		}
	}

    /**
     * Enum type representing types of type declarations
     */
    private enum TypeDeclarationEnum {
        CLASS, ENUM, ANNOTATION, INTERFACE;


        @Override
        public String toString() {
            return super.toString().toLowerCase();
        }
    }


}
